﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Text;
using System.Runtime.Serialization;

namespace Gargoyle.Roton
{
    public class BrokenSectionTagException : Exception, ISerializable
    {
        public BrokenSectionTagException() { }
        public BrokenSectionTagException(string message) { }
        public BrokenSectionTagException(string message, Exception inner) { }
        protected BrokenSectionTagException(SerializationInfo info, StreamingContext context) { }
    }

    public class SyntaxErrorException : Exception, ISerializable
    {
        public SyntaxErrorException() { }
        public SyntaxErrorException(string message) { }
        public SyntaxErrorException(string message, Exception inner) { }
        protected SyntaxErrorException(SerializationInfo info, StreamingContext context) { }
    }

    public static class ConfigManager
    {
        private static Hashtable currentConfig = new Hashtable();
        private const string GLOBAL_SECTION = "global";

        /// <summary>
        /// Describes a single configuration value.
        /// </summary>
        public struct ConfigValue
        {
            /// <summary>
            /// The section that the given key is in.
            /// </summary>
            public string SectionName;

            /// <summary>
            /// The name of the key.
            /// </summary>
            public string KeyName;

            /// <summary>
            /// The value of the key.
            /// </summary>
            public string Value;
        }

        private static string FormatConfigLine(string key, string value)
        {
            return String.Format("{0}={1}", key, value);
        }

        /// <summary>
        /// Returns a given section.
        /// </summary>
        /// <param name="Section">The name of the section to retrieve. Untagged sections can be acquired using the GLOBAL_SECTION constant.</param>
        /// <returns>A hashtable containing all of the key/value pairs in a section.</returns>
        public static Hashtable GetSection(string Section)
        {
            try
            {
                return (Hashtable)currentConfig[Section.ToLower()];
            }
            catch
            {
                return null;
            }
        }

        /// <summary>
        /// Returns a list of all of the valid sections in the configuration store.
        /// </summary>
        /// <returns>A list containing the names of all of the loaded sections.</returns>
        public static List<string> GetSections()
        {
            try
            {
                return currentConfig.Keys.Cast<string>().ToList<string>();
            }
            catch
            {
                return null;
            }
        }

        /// <summary>
        /// Returns a value in the configuration store.
        /// </summary>
        /// <param name="Section">The section of the configuration to read from.</param>
        /// <param name="Key">The key corresponding to the desired value.</param>
        /// <returns>The contents of the given section and key.</returns>
        public static string GetValue(string Section, string Key)
        {
            try
            {
                Hashtable section = (Hashtable)currentConfig[Section.ToLower()];
                return section[Key.ToLower()].ToString();
            }
            catch
            {
                return null;
            }
        }

        /// <summary>
        /// Returns a value in the configuration store.
        /// </summary>
        /// <param name="Section">The section of the configuration to read from.</param>
        /// <param name="Key">The key corresponding to the desired value.</param>
        /// <param name="Default">The default value if the section or key does not exist.</param>
        /// <returns>The contents of the given section and key.</returns>
        public static string GetValue(string Section, string Key, string Default)
        {
            try
            {
                Hashtable section = (Hashtable)currentConfig[Section.ToLower()];
                return section[Key.ToLower()].ToString();
            }
            catch
            {
                return Default;
            }
        }

        /// <summary>
        /// Loads a configuration file into the ConfigManager.
        /// </summary>
        /// <param name="Filename">The path to the config file.</param>
        /// <param name="Override">Specifies whether the values in the given configuration file should override the values that were previously read.</param>
        public static void LoadConfig(string Filename, bool Override)
        {
            FileStream fs = null;

            try
            {
                fs = new FileStream(Filename, FileMode.Open, FileAccess.Read, FileShare.Read);
                LoadConfig(fs, Override);
            }
            catch
            {
                // Exceptions should be handled by the calling class.
                throw;
            }
            finally
            {
                fs.Close();
            }
        }

        /// <summary>
        /// Loads a configuration file into the ConfigManager from a Stream.
        /// </summary>
        /// <param name="stream">The streaming with the configuration data.</param>
        /// <param name="Override">Specifies whether the values in the given configuration file should override the values that were previously read.</param>
        public static void LoadConfig(Stream stream, bool Override)
        {
            List<ConfigValue> values;

            try
            {
                values = ReadStream(stream);
                MergeConfig(values, Override);
            }
            catch
            {
                throw;
            }
        }

        /// <summary>
        /// Loads a configuration file into the ConfigManager from a TextReader.
        /// </summary>
        /// <param name="Reader">The TextReader instance to read from.</param>
        /// <param name="Override">Specifies whether the values in the given configuration file should override the values that were previously read.</param>
        public static void LoadConfig(TextReader Reader, bool Override)
        {
            List<ConfigValue> values;

            try
            {
                values = ReadStream(Reader);
                MergeConfig(values, Override);
            }
            catch
            {
                throw;
            }
        }

        private static void MergeConfig(List<ConfigValue> values, bool overrideExisting)
        {
            Hashtable table = null;
            string activeSection = null;

            foreach(ConfigValue value in values)
            {
                if(value.SectionName != activeSection)
                {
                    if(activeSection != null && currentConfig.ContainsKey(activeSection))
                        currentConfig[activeSection] = table;
                    else if(activeSection != null && !currentConfig.ContainsKey(activeSection))
                        currentConfig.Add(activeSection, table);

                    activeSection = value.SectionName;
                    if(currentConfig.ContainsKey(activeSection))
                        table = (Hashtable)currentConfig[activeSection];
                    else
                        table = new Hashtable();
                }

                if(value.SectionName == activeSection)
                {
                    if(!table.ContainsKey(value.KeyName))
                        table.Add(value.KeyName, value.Value);
                    else if(table.ContainsKey(value.KeyName) && overrideExisting)
                        table[value.KeyName] = value.Value;
                }
            }

            if(table != null)
            {
                if(currentConfig.ContainsKey(activeSection))
                    currentConfig[activeSection] = table;
                else
                    currentConfig.Add(activeSection, table);
            }
        }

        private static List<ConfigValue> ReadStream(Stream stream) {
            List<ConfigValue> inValues = new List<ConfigValue>();
            StreamReader sr = new StreamReader(stream);
            inValues = ReadStream(sr);
            sr.Close();
            return inValues;
        }

        private static List<ConfigValue> ReadStream(TextReader reader)
        {
            List<ConfigValue> inValues = new List<ConfigValue>();
            string currentSection = GLOBAL_SECTION.ToLower();

            while(true)
            {
                string inLine;

                try
                {
                    inLine = reader.ReadLine();
                    if (inLine == null)
                        break;
                    inLine = inLine.Trim();
                }
                catch(NullReferenceException)
                {
                    // end of stream
                    break;
                }

                if(inLine.Trim() == "")
                { } // no-op
                else if(inLine.Substring(0, 1) == "[")  // section definition
                {
                    if(inLine.Substring(inLine.Length - 1, 1) == "]")
                        currentSection = inLine.Substring(1, inLine.Length - 2).Trim();
                    else // formatting error
                        throw new BrokenSectionTagException(String.Format("Bad section tag ({0})", inLine));
                }
                else if(inLine.Substring(0, 1) == "#")  // comment
                { } // no-op
                else
                {
                    // key/value pair
                    if(inLine.Contains('='))
                    {
                        string[] split;
                        ConfigValue configValue = new ConfigValue();

                        split = inLine.Split('=');
                        if(split.Length == 2)
                        {
                            configValue.SectionName = currentSection.ToLower();
                            configValue.KeyName = split[0].Trim().ToLower();
                            configValue.Value = split[1].Trim();
                            inValues.Add(configValue);
                        }
                        else // too many values (or keys?)
                            throw new SyntaxErrorException(String.Format("Too many equal signs ({0})", inLine));
                    }
                    else // No equal sign! There's an exception for that...
                        throw new SyntaxErrorException(String.Format("Key/value pair expected ({0})", inLine));
                }
            }

            reader.Close();

            return inValues;
        }

        /// <summary>
        /// Saves a configuration file to a given file.
        /// </summary>
        /// <param name="Filename">The filename to output the configuration to.</param>
        public static void SaveConfig(string Filename)
        {
            FileStream fs = null;

            try
            {
                fs = new FileStream(Filename, FileMode.Create, FileAccess.Write, FileShare.Write);
                SaveConfig(fs);
            }
            catch
            {
                throw;
            }
            finally
            {
                fs.Close();
            }
        }

        /// <summary>
        /// Saves a configuration file to a given stream.
        /// </summary>
        /// <param name="stream">A stream to output the configuration to.</param>
        public static void SaveConfig(Stream stream)
        {
            StreamWriter sw = new StreamWriter(stream);
            List<string> sections = GetSections();
            Hashtable table;

            if(sections == null) return;

            // write out the global section first (if one exists), without a section tag
            if(sections.Contains(GLOBAL_SECTION))
            {
                sections.Remove(GLOBAL_SECTION);
                table = GetSection(GLOBAL_SECTION);

                foreach(DictionaryEntry entry in table)
                    sw.WriteLine(FormatConfigLine(entry.Key.ToString(), entry.Value.ToString()));
            }

            foreach(string section in sections)
            {
                table = GetSection(section);

                // add a space before the section tag for readability
                sw.WriteLine();
                sw.WriteLine(String.Format("[{0}]", section));

                foreach(DictionaryEntry entry in table)
                    sw.WriteLine(FormatConfigLine(entry.Key.ToString(), entry.Value.ToString()));
            }

            sw.Close();
        }

        /// <summary>
        /// Sets a value in the configuration store under the global section.
        /// </summary>
        /// <param name="Key">The key that the value should be stored under.</param>
        /// <param name="Value">The value to store in the given key.</param>
        public static void SetValue(string Key, string Value)
        {
            SetValue(null, Key, Value);
        }

        /// <summary>
        /// Sets a value in the configuration store.
        /// </summary>
        /// <param name="Section">The section that the key/value pair should be saved under.</param>
        /// <param name="Key">The key that the value should be stored under.</param>
        /// <param name="Value">The value to store in the given section and key.</param>
        public static void SetValue(string Section, string Key, string Value)
        {
            if(Section == null) Section = GLOBAL_SECTION.ToLower();

            Section = Section.ToLower();
            Key = Key.ToLower();

            Hashtable table;
            bool sectionExists = currentConfig.ContainsKey(Section);

            // retrieve existing section (if it exists) or create a new one
            if(sectionExists)
                table = (Hashtable)currentConfig[Section];
            else
                table = new Hashtable();

            // overwrite existing key (if it's set) or add it to the section
            if(table.ContainsKey(Key))
                table[Key] = Value;
            else
                table.Add(Key, Value);

            // replace existing section in the config store, or add the new section
            if(sectionExists)
                currentConfig[Section] = table;
            else
                currentConfig.Add(Section, table);
        }
    }
}
